﻿using System;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Web;
using System.Web.Configuration;
using System.Web.Mvc;
using System.Web.Security;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.RelyingParty;
using SPConf.Models;

namespace SPConf.Controllers
{
  [HandleError]
  public class AccountController : Controller
  {
    private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
    private ConfUsersRepository _confUsersRepository = new ConfUsersRepository();

    // This constructor is used by the MVC framework to instantiate the controller using
    // the default forms authentication and membership providers.
    public AccountController()
      : this(null, null)
    {
    }

    // This constructor is not used by the MVC framework but is instead provided for ease
    // of unit testing this type. See the comments at the end of this file for more
    // information.
    public AccountController(IFormsAuthentication formsAuth, IMembershipService service)
    {
      FormsAuth = formsAuth ?? new FormsAuthenticationService();
      MembershipService = service ?? new AccountMembershipService(_confUsersRepository);
    }

    public IFormsAuthentication FormsAuth { get; private set; }

    public IMembershipService MembershipService { get; private set; }

    public ActionResult LogOn()
    {
      ViewData["AppId"] = WebConfigurationManager.AppSettings["wll_appid"];
      return View();
    }

    public ActionResult ProfileUpdate(ConfUser user)
    {
      user.Id = User.Identity.Name;
      _confUsersRepository.Update(user);
      return RedirectToAction("Index", "Home");
    }

    public ActionResult Profile(string userId)
    {
      if (!User.Identity.IsAuthenticated)
        return RedirectToAction("Index", "Home");

      return View(_confUsersRepository.GetBy(User.Identity.Name));
    }

    [ValidateInput(false)]
    public ActionResult Authenticate(string returnUrl)
    {
      IAuthenticationResponse response = openid.GetResponse();
      if (response == null)
      {
        // Stage 2: user submitting Identifier
        Identifier id;
        if (Identifier.TryParse(Request.Form["openid_identifier"], out id))
        {
          try
          {
            IAuthenticationRequest request = openid.CreateRequest(Request.Form["openid_identifier"]);
            request.AddExtension(new ClaimsRequest
                                   {
                                     FullName = DemandLevel.Request,
                                     Email = DemandLevel.Request
                                   });
            return request.RedirectingResponse.AsActionResult();
          }
          catch (ProtocolException ex)
          {
            ViewData["Message"] = ex.Message;
            return View("Login");
          }
        }
        else
        {
          ViewData["Message"] = "Invalid identifier";
          return View("Login");
        }
      }
      else
      {
        // Stage 3: OpenID Provider sending assertion response
        switch (response.Status)
        {
          case AuthenticationStatus.Authenticated:
            MembershipUser user = MembershipService.CreateOrGetUser(response);
            FormsAuth.SignIn(user.UserName, true);

            if (!string.IsNullOrEmpty(returnUrl))
            {
              return Redirect(returnUrl);
            }
            else
            {
              return RedirectToAction("Profile", "Account");
            }
          case AuthenticationStatus.Canceled:
            ViewData["Message"] = "Canceled at provider";
            return View("Login");
          case AuthenticationStatus.Failed:
            ViewData["Message"] = response.Exception.Message;
            return View("Login");
        }
      }
      return new EmptyResult();
    }

    const string LoginCookie = "webauthtoken";
    static DateTime ExpireCookie = DateTime.Now.AddYears(-10);
    static DateTime PersistCookie = DateTime.Now.AddYears(10);
    static WindowsLiveLogin wll = new WindowsLiveLogin(true);

    [ValidateInput(false)]
    public ActionResult AuthenticateLiveId()
    {
      var req = HttpContext.Request;
      var res = HttpContext.Response;

      // Extract the 'action' parameter from the request, if any.
      string action = req["action"];

      if (action == "logout")
      {
        LogOut();
        return RedirectToAction("Index", "Home");
      }

      if (action == "clearcookie")
      {
        ClearCookie(res);
      }

      if (action == "login")
      {
        WindowsLiveLogin.User user = wll.ProcessLogin(req.Form);

        _confUsersRepository.CreateIfNotExist(user.Id, null, null);

        FormsAuth.SignIn(user.Id, true);

        HttpCookie loginCookie = new HttpCookie(LoginCookie);
        if (user != null)
        {
          loginCookie.Value = user.Token;

          if (user.UsePersistentCookie)
          {
            loginCookie.Expires = PersistCookie;
          }
        }
        else
        {
          loginCookie.Expires = ExpireCookie;
        }

        res.Cookies.Add(loginCookie);
        return RedirectToAction("Profile", "Account");
      }

      return View();
    }

    private void ClearCookie(HttpResponseBase res)
    {
      HttpCookie loginCookie = new HttpCookie(LoginCookie);
      loginCookie.Expires = ExpireCookie;
      res.Cookies.Add(loginCookie);

      string type;
      byte[] content;
      wll.GetClearCookieResponse(out type, out content);
    }

    public ActionResult LogOff()
    {
      ClearCookie(HttpContext.Response); 
      LogOut();
      return RedirectToAction("Index", "Home");
    }

    private void LogOut()
    {
      FormsAuth.SignOut();
      HttpResponseBase res = HttpContext.Response;
      HttpCookie loginCookie = new HttpCookie(LoginCookie);
      loginCookie.Expires = ExpireCookie;
      res.Cookies.Add(loginCookie);
    }

    // TODO: do we need this? find out and remove if not.
    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      if (filterContext.HttpContext.User.Identity is WindowsIdentity)
      {
        throw new InvalidOperationException("Windows authentication is not supported.");
      }
    }
  }

  // The FormsAuthentication type is sealed and contains static members, so it is difficult to
  // unit test code that calls its members. The interface and helper class below demonstrate
  // how to create an abstract wrapper around such a type in order to make the AccountController
  // code unit testable.

  public interface IFormsAuthentication
  {
    void SignIn(string userName, bool createPersistentCookie);
    void SignOut();
  }

  public class FormsAuthenticationService : IFormsAuthentication
  {
    #region IFormsAuthentication Members

    public void SignIn(string userName, bool createPersistentCookie)
    {
      FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
    }

    public void SignOut()
    {
      FormsAuthentication.SignOut();
    }

    #endregion
  }

  public interface IMembershipService
  {
    MembershipUser CreateOrGetUser(IAuthenticationResponse response);
  }

  public class AccountMembershipService : IMembershipService
  {
    private static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider();
    private readonly ConfUsersRepository _repository;
    private MembershipProvider provider;

    public AccountMembershipService(ConfUsersRepository repository)
      : this(Membership.Provider)
    {
      _repository = repository;
    }

    public AccountMembershipService(MembershipProvider provider)
    {
      this.provider = provider ?? Membership.Provider;
    }

    #region IMembershipService Members

    public MembershipUser CreateOrGetUser(IAuthenticationResponse response)
    {
      MembershipUser user = provider.GetUser(response.ClaimedIdentifier, true);

      var claimsResponse = response.GetExtension<ClaimsResponse>();
      string email = null;
      string fullName = null;
      if (claimsResponse != null)
      {
        email = claimsResponse.Email;
        fullName = claimsResponse.FullName;
      }


      if (user == null)
      {
        MembershipCreateStatus status;
        user = provider.CreateUser(response.ClaimedIdentifier,
                                   GenerateRandomString(64),
                                   email,
                                   "This is an OpenID account. You should log in with your OpenID.",
                                   GenerateRandomString(64),
                                   true,
                                   null,
                                   out status);
        if (status != MembershipCreateStatus.Success)
        {
          throw new Exception("Failed to find or create user: " + status);
        }

        // TODO: set extra info in the profile, taking it from OpenID.
      }
      _repository.CreateIfNotExist(user.UserName, email, fullName);
      return user;
    }

    #endregion

    private static string GenerateRandomString(int length)
    {
      var buffer = new byte[length];
      CryptoRandomDataGenerator.GetBytes(buffer);
      return Convert.ToBase64String(buffer);
    }
  }
}